package qiniu

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	"mall/common/globalkey"
	"mall/common/modelx"
	"mall/common/utils/pointy"
	"mall/service/attachment/model"
	"mall/service/attachment/rpc/internal/svc"
	"mall/service/attachment/rpc/internal/utils/errorhandler"
	"mall/service/attachment/rpc/types/attachment"
	"mall/service/backend/common/errorx"
	"mall/service/backend/common/i18n"

	"github.com/qiniu/go-sdk/v7/auth/qbox"
	"github.com/qiniu/go-sdk/v7/storage"
	"github.com/sirupsen/logrus"
	"github.com/volatiletech/null/v8"
	"github.com/zeromicro/go-zero/core/logx"
)

type QiniuPutFileLogic struct {
	ctx           context.Context
	svcCtx        *svc.ServiceContext
	bucketManager *storage.BucketManager
	bucket        string
	config        *storage.Config
	logx.Logger
}

func NewQiniuPutFileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *QiniuPutFileLogic {
	return &QiniuPutFileLogic{
		ctx:    ctx,
		svcCtx: svcCtx,
		Logger: logx.WithContext(ctx),
	}
}

// 文件上传（表单方式）
func (l *QiniuPutFileLogic) QiniuPutFile(in *attachment.RecordInfo) (*attachment.RecordInfo, error) {
	mac := qbox.NewMac(l.svcCtx.Config.QiniuConf.AccessKey, l.svcCtx.Config.QiniuConf.SecretKey)

	cfg := &storage.Config{
		UseHTTPS:      l.svcCtx.Config.QiniuConf.UseHTTPS,      // 是否使用https域名进行资源管理
		UseCdnDomains: l.svcCtx.Config.QiniuConf.UseCdnDomains, // 上传是否使用CDN上传加速
	}

	// 指定空间所在的区域，如果不指定将自动探测
	// 如果没有特殊需求，默认不需要指定
	// cfg.Region=&storage.ZoneHuabei
	// switch l.svcCtx.Config.QiniuConf.Region {
	// case "z2":
	// 	cfg.Region = &storage.ZoneHuanan
	// }

	l.bucketManager = storage.NewBucketManager(mac, cfg)
	l.bucket = l.svcCtx.Config.QiniuConf.Bucket
	l.config = cfg

	file, err := l.findAttachment(in)
	if err != nil {
		return nil, err
	}

	if file == nil || len(file.Path) == 0 || file.PostStatus == uint16(globalkey.AttachmentStatusTrash) {
		return nil, errorx.NewNotFoundError(i18n.TargetNotFound)
	}

	var destBucket string
	if in.Bucket != nil && len(strings.TrimSpace(*in.Bucket)) > 0 {
		destBucket = strings.TrimSpace(*in.Bucket)
	}
	if len(destBucket) == 0 {
		destBucket = l.bucket
	}

	var destKey string // 不包含左侧的“/”
	if in.Key != nil && len(strings.TrimSpace(*in.Key)) > 0 {
		destKey = strings.TrimSpace(*in.Key)
	}
	if len(destKey) == 0 {
		destKey = strings.TrimLeft(file.Path, l.svcCtx.Config.NfsConf.QiniuPrefix)
	}
	var localFile string = l.svcCtx.Config.NfsConf.PathPrefix + file.Path
	// /mnt/data/nfs/scentrobe-attachment-pvc-pvc-562cdddf-7af5-40f8-8d56-8aa596dca949/ => /app/static/

	putPolicy := storage.PutPolicy{
		Scope: destBucket,
	}
	upToken := putPolicy.UploadToken(mac)

	// 构建表单上传的对象
	formUploader := storage.NewFormUploader(l.config)
	ret := storage.PutRet{}
	// 可选配置
	putExtra := storage.PutExtra{
		Params: map[string]string{
			"x:name": "Server Upload",
		},
	}

	err = formUploader.PutFile(context.Background(), &ret, upToken, destKey, localFile, &putExtra)
	if err != nil {
		logrus.Info(fmt.Sprintf("QiniuPutFile err: %+v", err))
		logx.Errorw("QiniuPutFile err", logx.Field("detail", err.Error()))
		return nil, errorx.NewInternalError("file.updateFileFailed")
	}

	logrus.Info(fmt.Sprintf("Qiniu Put: %+v", ret))
	// {Hash:FlsEzctYF6lTDhazCZ4OkOyE65MI PersistentID: Key:uploads/image/202310/018b1311-f7a1-7204-aa33-7ca964d50185.jpg}
	// {Hash:FoLyHcm46RCsQMQ0_lrnx8mgSto8 PersistentID: Key:ets/image/scentrobe/logo-v.png}
	// assets 会被七牛云强行替换成 ets

	info, err := l.findInfo(file.ID, ret.Key, 0)
	if err != nil {
		return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
	}

	if info != nil {
		if info.AttachmentID == 0 { // 关联操作
			info.AttachmentID = file.ID
			err = l.svcCtx.FileInfo.Update(l.ctx, nil, info)
			if err != nil {
				return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
			}
		}
	} else {
		var newInfo model.AttachmentFileInfo
		newInfo.AttachmentID = file.ID
		newInfo.KeyValue = ret.Key
		newInfo.Hash = ret.Hash
		newInfo.PersistentID = ret.PersistentID
		res, err := l.createOrUpdate(destBucket, destKey, &newInfo)
		if err != nil {
			return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
		}

		info = res
	}

	newType := l.getAttachmentType(int32(file.PostType), "put")
	if len(file.GUID) == 0 || newType > 0 {
		if len(file.GUID) == 0 {
			file.GUID = "/" + ret.Key
		}
		if newType > 0 {
			file.PostType = uint16(newType)
		}
		err = l.svcCtx.MediaFile.Update(l.ctx, nil, file, nil)
		// UPDATE `attachments` SET `post_author_id`=1,`post_parent_id`=43011,`post_title`="logo",`post_excerpt`="Logo of NOSE'S NOOK",`post_status`=11,`del_status`=0,`comment_status`=0,`post_password`="",`post_name`="logo-v.png",`guid`="/ets/image/scentrobe/logo-v.png",`post_date`="2025-02-12 19:19:59 +0800 CST",`post_date_gmt`="2025-02-12 11:19:59 +0800 CST",`post_modified_date`="2025-02-18 15:23:35 +0800 CST",`post_modified_gmt`="2025-02-18 15:23:35 +0800 CST",`menu_order`=0,`post_type`=12,`post_mime_type`="image/png",`comment_count`=0,`path`="/static/assets/image/scentrobe/logo-v.png",`size`=0,`md5`="",`count`=0 WHERE `ID`=49691;
		if err != nil {
			return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
		}
	}

	if info == nil {
		return &attachment.RecordInfo{}, nil
	}

	var updatedAt *string
	if info.PutTime > 0 {
		updatedAt = pointy.GetPointer(modelx.FormatTime(time.Unix(0, int64(info.PutTime)*100), ""))
	}
	var expiredAt *string
	if info.Expiration > 0 {
		expiredAt = pointy.GetPointer(modelx.FormatTime(time.Unix(int64(info.Expiration), 0), ""))
	}
	var meta *string
	if info.MetaValue.Valid {
		meta = pointy.GetPointer(info.MetaValue.String)
	}

	return &attachment.RecordInfo{
		Id:            pointy.GetPointer(strconv.FormatUint(info.ID, 10)),
		FileId:        &info.AttachmentID,
		ParentId:      &info.ParentID,
		Bucket:        &info.Bucket,
		Key:           &info.KeyValue,
		Size:          &info.Size,
		Hash:          &info.Hash,
		MimeType:      &info.MimeType,
		Type:          pointy.GetPointer(uint32(info.Type)),
		Status:        pointy.GetPointer(uint32(info.Status)),
		RestoreStatus: pointy.GetPointer(uint32(info.RestoreStatus)),
		Md5:           &info.MD5,
		EndUser:       &info.EndUser,
		MetaValue:     meta,
		DelStatus:     pointy.GetPointer(uint32(info.DelStatus)),
		UpdatedAt:     updatedAt,
		ExpiredAt:     expiredAt,
	}, nil
}

func (l *QiniuPutFileLogic) findAttachment(in *attachment.RecordInfo) (*model.Attachment, error) {
	if in.Id != nil && len(strings.TrimSpace(*in.Id)) > 0 {
		if i, err := strconv.ParseInt(strings.TrimSpace(*in.Id), 10, 64); err == nil && i > 0 {
			res, err := l.svcCtx.MediaFile.FindOneNoCache(l.ctx, nil, uint64(i))
			if err != nil {
				return nil, errorhandler.DefaultRepositoryError(l.Logger, err, in)
			}

			return res, nil
		}
	}

	return nil, errorx.NewInvalidArgumentError(i18n.ValidationError)
}

func (l *QiniuPutFileLogic) createOrUpdate(
	bucket string,
	key string,
	data *model.AttachmentFileInfo,
) (*model.AttachmentFileInfo, error) {
	fileInfo, err := l.bucketManager.Stat(bucket, key)
	if err != nil {
		logrus.Info(fmt.Sprintf("QiniuPutFile Stat err: %+v", err))
		logx.Errorw("QiniuPutFile Stat err", logx.Field("detail", err.Error()))
		// Qiniu Stat err: no such file or directory
		return nil, err
	}

	var newData *model.AttachmentFileInfo

	if data != nil && data.ID > 0 {
		newData = data
	} else {
		newData = &model.AttachmentFileInfo{}
	}

	newData.KeyValue = key
	newData.Bucket = bucket
	newData.Size = uint64(fileInfo.Fsize)
	newData.Hash = fileInfo.Hash
	newData.MimeType = fileInfo.MimeType
	newData.Type = uint8(fileInfo.Type)
	newData.PutTime = uint64(fileInfo.PutTime)
	newData.RestoreStatus = uint8(fileInfo.RestoreStatus)
	newData.Status = uint8(fileInfo.Status)
	newData.MD5 = fileInfo.Md5
	newData.EndUser = fileInfo.EndUser
	newData.Expiration = uint64(fileInfo.Expiration)
	newData.TransitionToIa = uint64(fileInfo.TransitionToIA)
	newData.TransitionToArchive = uint64(fileInfo.TransitionToArchive)
	newData.TransitionToDeepArchive = uint64(fileInfo.TransitionToDeepArchive)
	if len(fileInfo.Parts) > 0 {
		var parts []string
		for _, partId := range fileInfo.Parts {
			parts = append(parts, strconv.FormatInt(partId, 10))
		}
		newData.Parts = strings.Join(parts, ",")
	}
	if fileInfo.MetaData != nil {
		var items []string
		for mk, mv := range fileInfo.MetaData {
			items = append(items, mk+":"+mv)
		}
		newData.MetaValue = null.StringFrom(strings.Join(items, ";"))
	}

	if newData.ID > 0 {
		err := l.svcCtx.FileInfo.Update(l.ctx, nil, newData)
		if err != nil {
			return nil, err
		}

		return newData, nil
	}

	if data != nil {
		if data.AttachmentID > 0 {
			newData.AttachmentID = data.AttachmentID
		}
		if len(data.PersistentID) > 0 {
			newData.PersistentID = data.PersistentID
		}
		if data.ParentID > 0 {
			newData.ParentID = data.ParentID
		}
	}
	err = l.svcCtx.FileInfo.Insert(l.ctx, nil, newData)
	if err != nil {
		return nil, err
	}

	return newData, err
}

func (l *QiniuPutFileLogic) findInfo(id uint64, key string, pid uint64) (*model.AttachmentFileInfo, error) {
	filterMap := make(map[string]map[string][]any)
	filterMap[model.AttachmentFileInfoColumns.KeyValue] = map[string][]any{
		"equalTo": {key},
	}
	filterMap[model.AttachmentFileInfoColumns.ParentID] = map[string][]any{
		"equalTo": {pid},
	}
	filterMap[model.AttachmentFileInfoColumns.DelStatus] = map[string][]any{
		"notEqualTo": {globalkey.StatusDeleted},
	}
	list, count, err := l.svcCtx.FileInfo.GetList(l.ctx, model.M{
		"pageSize": uint64(1),
		"orderBy":  model.AttachmentFileInfoColumns.ID + " DESC",
	}, 1, filterMap)
	if err != nil {
		return nil, err
	}

	var info *model.AttachmentFileInfo
	if count > 1 { // 记录重复异常
		info = list[0]

		var metaVals []string
		for _, file := range list {
			metaVals = append(metaVals, strconv.FormatUint(file.ID, 10))
		}

		var log model.AttachmentFileInfoLog
		log.AttachmentID = id
		log.InfoID = info.ID
		log.Type = uint8(globalkey.AttachmentLogInfoDuplicated)
		log.KeyValue = key
		log.Count = uint64(count)
		log.MetaValue = strings.Join(metaVals, ",")
		err = l.svcCtx.FileInfo.InsertLog(l.ctx, nil, &log)
		if err != nil {
			return nil, err
		}

		return info, nil
	}

	if count == 1 {
		info = list[0]

		if info.AttachmentID > 0 && info.AttachmentID != id { // 记录重复异常
			var log model.AttachmentFileInfoLog
			log.AttachmentID = id
			log.InfoID = info.ID
			log.Type = uint8(globalkey.AttachmentLogDuplicated)
			log.KeyValue = key
			log.Count = uint64(count)
			err = l.svcCtx.FileInfo.InsertLog(l.ctx, nil, &log)
			if err != nil {
				return nil, err
			}
		}

		return info, nil
	}

	return nil, nil
}

func (l *QiniuPutFileLogic) getAttachmentType(fileType int32, op string) int32 {
	var newFileType int32
	switch op {
	case "put":
		switch fileType {
		case globalkey.AttachmentTypePutPendingFile:
			newFileType = globalkey.AttachmentTypePutReadyFile
		case globalkey.AttachmentTypeWatermarkPendingFile:
			newFileType = globalkey.AttachmentTypeWatermarkPendingPutReadyFile
		case globalkey.AttachmentTypeThumbnailPendingFile:
			newFileType = globalkey.AttachmentTypeThumbnailPendingPutReadyFile
		case globalkey.AttachmentTypePendingFile:
			newFileType = globalkey.AttachmentTypePendingPutReadyFile
		default:
			newFileType = globalkey.AttachmentTypePutReadyFile
		}
	case "thumbnail":
		switch fileType {
		case globalkey.AttachmentTypeThumbnailPendingPutReadyFile:
			newFileType = globalkey.AttachmentTypeThumbnailReadyFile
		case globalkey.AttachmentTypePendingWatermarkReadyFile:
			newFileType = globalkey.AttachmentTypeReadyFile
		default:
			newFileType = globalkey.AttachmentTypeThumbnailReadyFile
		}
	case "watermark":
		switch fileType {
		case globalkey.AttachmentTypeWatermarkPendingPutReadyFile:
			newFileType = globalkey.AttachmentTypeWatermarkReadyFile
		case globalkey.AttachmentTypePendingPutReadyFile:
			newFileType = globalkey.AttachmentTypePendingWatermarkReadyFile
		default:
			newFileType = globalkey.AttachmentTypeWatermarkReadyFile
		}
	}

	return newFileType
}
